"""
Copyright 2009 Texas A&M University
 
Licensed under the Apache License, Version 2.0 (the "License"); you may not use 
this file except in compliance with the License. You may obtain a copy of the 
License at

http://www.apache.org/licenses/LICENSE-2.0
  
Unless required by applicable law or agreed to in writing, software distributed 
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 
CONDITIONS OF ANY KIND, either express or implied. See the License for the 
specific language governing permissions and limitations under the License. 
"""

from logging import getLogger
import httplib
from urllib import urlencode

try:
    from xml.etree import cElementTree as etree
except ImportError:
    from xml.etree import ElementTree as etree

import zope.interface

from metalib.interfaces import IMetalibConnection
from metalib.exceptions import *
from metalib.session import MetalibSession



__all__  = ['MetalibConnection']

log = getLogger('metalib.connection')

def convertValueToElement(tag, value):
    tag = etree.Element(tag)
    if isinstance(value,basestring):
        tag.text = value
    elif isinstance(value,dict):
        map(tag.append,convertDictToElement(value))
    else:
        raise ValueError('convertValueToElement converts strings or '
                         'dictionaries to a single tag, no other value types '
                         'allowed')
    return tag

def convertDictToElement(data):
    """ Data is dictionary of tag:values """
    tags = []
    
    for tag, value in data.iteritems():
        if isinstance(value,basestring) or isinstance(value,dict):
            tags.append(convertValueToElement(tag, value))
        else:
            for v in value:
                tags.append(convertValueToElement(tag,v))
    
    return tags



class MetalibConnection(object):
    zope.interface.implements(IMetalibConnection)
    
    def __init__(self, host, port='8331', path='/X', timeout=None):
        self._connection_arguments = {'host':host,
                                      'port':port}
        
        if timeout is not None:
            self._connection_arguments['timeout'] = timeout
        
        self._path = path
        self._connection = None
    
    def _getConnection(self):
        if self._connection is None:
            self._connection = httplib.HTTPConnection(**self._connection_arguments)
        
        return self._connection
    
    def post(self, operation, **kwargs):
        """ Encode the operation and the provided arguments into the xml input
        format required by the metalib server """
        data = etree.tostring(convertDictToElement({'x_server_request':{operation:kwargs}})[0],
                              encoding='utf-8')
        
        data = u'<?xml version="1.0" encoding="UTF-8"?>' + data
        try:        
            conn = self._getConnection()
            conn.connect()
            try:
                conn.request('POST',self._path,urlencode({'xml':data}))
                return self._handleResponse(conn.getresponse())
            except:
                raise
            finally:
                conn.close()
        except:
            raise
    def _handleResponse(self, response):
        """ Check the response for errors and parse as necessary """
        if response.status != 200:
            raise MetalibException(response.read())
        
        response = etree.fromstring(response.read())
        
        #TODO: Check response for errors before return to calling code, if an error is encountered, raise an appropriate exception
        
        return response
    
    def connect(self, username, password):
        """ Return a new IMetalibSession object """
        log.info('%s.connect()' % (str(self),))
        try:
            response = self.post('login_request',user_name=username,
                                 user_password=password)
        except:
            raise
        session_id = response.find('*/session_id')
        auth = response.find('*/auth')
        
        if session_id is not None and session_id.text:
            return MetalibSession(connection=self, id=session_id.text)
        
        if auth is not None and auth.text == 'N':
            raise MetalibException('Invalid username or password')
        
        local_error = response.find('*/local_error')
        if local_error is not None:
          error_code = response.find('*/local_error/error_code').text
          error_msg = response.find('*/local_error/error_text').text
          raise MetalibServerException(error_code, error_msg)
          
        
        #TODO: Handle an invalid xml response (non-error)
        raise MetalibException(etree.tostring(response))
